/*
* Copyright 2015 Bounce Storage, Inc. <info@bouncestorage.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.bouncestorage.swiftproxy.v1;
import static java.util.Objects.requireNonNull;
import static com.google.common.base.Throwables.propagate;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import javax.validation.constraints.NotNull;
import javax.ws.rs.BadRequestException;
import javax.ws.rs.DELETE;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.HEAD;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlType;
import com.bouncestorage.swiftproxy.BlobStoreResource;
import com.bouncestorage.swiftproxy.BounceResourceConfig;
import com.bouncestorage.swiftproxy.Utils;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.DateSerializer;
import com.google.common.base.Strings;
import org.jclouds.blobstore.BlobStore;
import org.jclouds.blobstore.domain.BlobMetadata;
import org.jclouds.blobstore.domain.StorageMetadata;
import org.jclouds.blobstore.domain.StorageType;
import org.jclouds.blobstore.options.ListContainerOptions;
@Path("/v1/{account}/{container}")
public final class ContainerResource extends BlobStoreResource {
private void createContainer(String authToken, String container) {
if (container.length() > InfoResource.CONFIG.swift.max_container_name_length) {
throw new BadRequestException("container name too long");
}
getBlobStore(authToken).get(container).createContainerInLocation(null, container);
}
@POST
public Response postContainer(@NotNull @PathParam("container") String container,
@HeaderParam("X-Auth-Token") String authToken,
@HeaderParam("X-Container-Read") String readACL,
@HeaderParam("X-Container-write") String writeACL,
@HeaderParam("X-Container-Sync-To") String syncTo,
@HeaderParam("X-Container-Sync-Key") String syncKey,
@HeaderParam("X-Versions-Location") String versionsLocation,
@HeaderParam(HttpHeaders.CONTENT_TYPE) String contentType,
@HeaderParam("X-Detect-Content-Type") boolean detectContentType,
@HeaderParam(HttpHeaders.IF_NONE_MATCH) String ifNoneMatch) {
createContainer(authToken, container);
return Response.status(Response.Status.NO_CONTENT).build();
}
@PUT
public Response putContainer(@NotNull @PathParam("container") String container,
@HeaderParam("X-Auth-Token") String authToken,
@HeaderParam("X-Container-Read") String readACL,
@HeaderParam("X-Container-write") String writeACL,
@HeaderParam("X-Container-Sync-To") String syncTo,
@HeaderParam("X-Container-Sync-Key") String syncKey,
@HeaderParam("X-Versions-Location") String versionsLocation,
@HeaderParam(HttpHeaders.CONTENT_TYPE) String contentType,
@HeaderParam("X-Detect-Content-Type") boolean detectContentType,
@HeaderParam(HttpHeaders.IF_NONE_MATCH) String ifNoneMatch) {
Response.Status status;
BlobStore store = getBlobStore(authToken).get(container);
if (store.containerExists(container)) {
status = Response.Status.ACCEPTED;
} else {
createContainer(authToken, container);
status = Response.Status.CREATED;
}
return Response.status(status).build();
}
@DELETE
public Response deleteContainer(@NotNull @PathParam("container") String container,
@HeaderParam("X-Auth-Token") String authToken) {
BlobStore store = getBlobStore(authToken).get(container);
if (!store.containerExists(container)) {
return Response.status(Response.Status.NOT_FOUND).build();
}
if (store.deleteContainerIfEmpty(container)) {
return Response.noContent().build();
} else {
return Response.status(Response.Status.CONFLICT)
.entity("<html><h1>Conflict</h1><p>There was a conflict when trying to complete your request.</p></html>")
.build();
}
}
@HEAD
public Response headContainer(@NotNull @PathParam("container") String container,
@HeaderParam("X-Auth-Token") String authToken,
@HeaderParam("X-Newest") @DefaultValue("false") boolean newest) {
BlobStore store = getBlobStore(authToken).get(container);
if (!store.containerExists(container)) {
return Response.status(Response.Status.NOT_FOUND).build();
}
long objectCount = -1;
String provider = store.getContext().unwrap().getId();
if (provider.equals("transient") || provider.equals("openstack-swift")) {
objectCount = store.countBlobs(container);
}
return Response.status(Response.Status.NO_CONTENT).entity("")
.header("X-Container-Object-Count", objectCount)
.header("X-Container-Bytes-Used", 0) // TODO: bogus value
.header("X-Versions-Location", "")
.header("X-Timestamp", -1)
.header("X-Trans-Id", -1)
.header("Accept-Ranges", "bytes")
.build();
}
private String contentType(StorageMetadata meta) {
if (meta instanceof BlobMetadata) {
String contentType = ((BlobMetadata) meta).getContentMetadata().getContentType();
if (contentType != null && !contentType.isEmpty()) {
return contentType;
}
}
if (meta.getType().equals(StorageType.RELATIVE_PATH) || meta.getName().endsWith("/")) {
return "application/directory";
}
return MediaType.APPLICATION_OCTET_STREAM;
}
@GET
public Response listContainer(@NotNull @PathParam("container") String container,
@HeaderParam("X-Auth-Token") String authToken,
@QueryParam("limit") Integer limit,
@QueryParam("marker") String marker,
@QueryParam("end_marker") String endMarker,
@QueryParam("format") Optional<String> format,
@QueryParam("prefix") String prefixParam,
@QueryParam("delimiter") String delimiterParam,
@QueryParam("path") String path,
@HeaderParam("X-Newest") @DefaultValue("false") boolean newest,
@HeaderParam("Accept") Optional<String> accept) {
BlobStore store = getBlobStore(authToken).get(container);
if (!store.containerExists(container)) {
return Response.status(Response.Status.NOT_FOUND).build();
}
ListContainerOptions options = new ListContainerOptions();
if (!Strings.isNullOrEmpty(marker)) {
options.afterMarker(marker);
}
if (Strings.isNullOrEmpty(delimiterParam) && path == null) {
options.recursive();
}
if (!Strings.isNullOrEmpty(delimiterParam)) {
options.delimiter(delimiterParam);
}
if (!Strings.isNullOrEmpty(prefixParam)) {
options.prefix(prefixParam);
}
if (path != null) {
if (path.equals("/")) {
options.prefix("/");
options.delimiter("/");
} else {
options.inDirectory(path);
}
}
logger.info("list: {} marker={} prefix={}", options, options.getMarker(), prefixParam);
List<ObjectEntry> entries = StreamSupport.stream(
Utils.crawlBlobStore(store, container, options).spliterator(), false)
.peek(meta -> logger.debug("meta: {}", meta))
//.filter(meta -> (prefix == null || meta.getName().startsWith(prefix)))
//.filter(meta -> delimFilter(meta.getName(), delim_filter))
.filter(meta -> endMarker == null || meta.getName().compareTo(endMarker) < 0)
.limit(limit == null ? InfoResource.CONFIG.swift.container_listing_limit : limit)
.map(meta -> new ObjectEntry(meta.getName(), meta.getETag(),
meta.getSize() == null ? 0 : meta.getSize(),
contentType(meta), meta.getLastModified()))
.collect(Collectors.toList());
MediaType formatType;
if (format.isPresent()) {
formatType = BounceResourceConfig.getMediaType(format.get());
} else if (accept.isPresent()) {
formatType = MediaType.valueOf(accept.get());
} else {
formatType = MediaType.TEXT_PLAIN_TYPE;
}
if (store.getContext().unwrap().getId().equals("transient")) {
entries.forEach(entry -> {
try {
entry.name = URLDecoder.decode(entry.name, "UTF-8");
} catch (UnsupportedEncodingException e) {
throw propagate(e);
}
});
}
// XXX semi-bogus value
long totalBytes = entries.stream().mapToLong(e -> e.bytes).sum();
ContainerRoot root = new ContainerRoot();
root.name = container;
root.object = entries;
return output(root, entries, formatType)
.header("X-Container-Object-Count", entries.size())
.header("X-Container-Bytes-Used", totalBytes)
.header("X-Timestamp", -1)
.header("X-Trans-Id", -1)
.header("Accept-Ranges", "bytes")
.build();
}
@XmlRootElement(name = "container")
@XmlType
static class ContainerRoot {
@XmlElement
List<ObjectEntry> object;
@XmlAttribute
private String name;
}
@XmlRootElement(name = "object")
@XmlType
static class ObjectEntry {
static class SwiftDateSerializer extends DateSerializer {
SwiftDateSerializer() {
super(false, new SimpleDateFormat("yyyy-MM-dd'T'kk:mm:ss.SSSSSS"));
}
}
@XmlElement
String name;
@XmlElement
String hash;
@XmlElement
long bytes;
@XmlElement
String content_type;
@JsonSerialize(using = SwiftDateSerializer.class)
@XmlElement
Date last_modified;
// dummy
public ObjectEntry() {
}
@JsonCreator
public ObjectEntry(@JsonProperty("name") String name,
@JsonProperty("hash") String hash,
@JsonProperty("bytes") long bytes,
@JsonProperty("content_type") String content_type,
@JsonProperty("last_modified") Date last_modified) {
this.name = requireNonNull(name);
this.hash = hash == null ? "" : Utils.trimETag(hash);
this.bytes = bytes;
this.content_type = requireNonNull(content_type);
this.last_modified = last_modified == null ? Date.from(Instant.EPOCH) : last_modified;
}
@Override
public String toString() {
return name;
}
}
}